package com.suprema.biomini;

import com.sun.jna.Memory;
import com.sun.jna.Native;
import com.sun.jna.Pointer;
import com.sun.jna.platform.win32.WinDef.HWND;
import com.sun.jna.ptr.IntByReference;
import idv.kentYeh.util.Supplier;
import java.awt.Component;
import java.awt.Dimension;
import java.awt.Graphics;
import java.awt.image.BufferedImage;
import java.awt.image.IndexColorModel;
import java.awt.image.RenderedImage;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.io.OutputStream;
import java.util.Arrays;
import java.util.List;
import java.util.concurrent.atomic.AtomicBoolean;
import javax.imageio.ImageIO;
import javax.imageio.stream.ImageOutputStream;
import javax.swing.event.EventListenerList;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

/**
 * <span style="color:blue;font-weight:bold">包裝 SFR指紋機 相關功能.</span>
 * <div style="color:red;font-weight:bold">必須在系統變數 &quot;jna.library.path&quot; 內設定SFR300.dll 之目錄路徑,
 * 或是在Window下把SFR300.dll目錄路徑加到環境變數 PATH 內</div>
 * <dl>
 *     <dt style="color:blue;font-weight:bold">註冊流程</dt>
 *     <dd>清除:SF_Clear<br>
 * 如果 掃瞄:SF_Capture 成功{<br>
 * &nbsp;&nbsp;&nbsp;&nbsp;如果 取樣:SF_Enroll 成功{<br>
 * &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;def 品質 = 取得品質:SF_GetEnrollQuality<br>
 * &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;如果 品質良好{<br>
 * &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;如果 需再確認{<br>
 * &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;如果 掃瞄:SF_Capture 成功{<br>
 * &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;如果 執行確認:SF_EnrollWithVerify 成功{<br>
 * &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;如果 取得品質:SF_GetEnrollQuality 良好<br>
 * &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;-->完成註冊<br>
 * &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;}<br>
 * &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;}<br>
 * &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;}<br>
 * &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;-->完成註冊<br>
 * &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;}<br>
 * &nbsp;&nbsp;&nbsp;&nbsp;}&nbsp;<br>
 * } </dd>
 *   <dt style="color:blue;font-weight:bold">1:1 識別流程</dt>
 *   <dd>清除:SF_Clear<br/>
 * 如果 掃瞄:SF_Capture 成功{<br/>
 * &nbsp;&nbsp;&nbsp;&nbsp;是否成功 = 比對:SF_Verify(樣本) <br/>
 * }
 *   </dd>
 *   <dt style="color:blue;font-weight:bold">1:N 識別流程</dt>
 *   <dd>清除:SF_Clear<br/>
 * 如果 掃瞄:SF_Capture 成功{<br/>
 * &nbsp;&nbsp;&nbsp;&nbsp;是否成功 = 比對:SF_Identify[MT](樣本母體) <br/>
 * }
 *   </dd>
 * </dl>
 * @author Kent Yeh
 */
public class SFRDevice {

    protected EventListenerList listenerList = new EventListenerList();
    protected static Logger logger = LoggerFactory.getLogger(SFRDevice.class);
    private static SFRDevice instance = new SFRDevice();
    private static AtomicBoolean initialized = new AtomicBoolean(false);
    private static DeviceType deviceType = null;
    private static SFR300 device = null;
    private static AtomicBoolean captured = new AtomicBoolean(false);
    private static AtomicBoolean inCapturing = new AtomicBoolean(false);
    private static AtomicBoolean inMatching = new AtomicBoolean(false);
    private static boolean fastMode = false;

    private SFRDevice() {
    }

    private void reset() {
        abortCapturing();
        setFastMode(false);
    }

    private synchronized void ensureInitialize() {
        if (!initialized.getAndSet(true)) {
            device = SFR300Loader.getInstance();
            deviceType = device.SF_Initialize();
            if (deviceType.equals(DeviceType.SFR_NONE)) {
                logger.error("函式初始化失敗:找不到任何設備");
                device = null;
                initialized.set(false);
            } else {
                logger.debug("載入dll函式:{}", deviceType.toString());
                Runtime.getRuntime().addShutdownHook(new Thread(new Runnable() {

                    public void run() {
                        unload();
                    }
                }));
            }
        }
    }

    /**
     * 取得指紋機設備
     * @return 
     */
    public static SFRDevice getInstance() {
        return instance;
    }

    /**
     * 檢驗是否已掃瞄(前次掃瞄失敗或呼叫 clear() 或 abortCapturing()後則回傳False)
     * @return 
     */
    public static boolean isCaptured() {
        return captured.get();
    }

    /**
     * 快速模式，快速模式是為了加速1:N 比對
     * @return 快速模式N
     */
    public static boolean isFastMode() {
        return fastMode;
    }

    /**
     * 設定 快速模式 以加速 1:N 比對
     * @param  fastMode 快速模式
     * @return 設定後的快速模式(以回傳檢驗是否設定成功)
     */
    public static boolean setFastMode(boolean fastMode) {
        if (device != null && SFRDevice.fastMode != fastMode) {
            if (device.SF_SetFastMode(fastMode) != -1) {
                SFRDevice.fastMode = fastMode;
            }
        } else {
            return false;
        }
        return fastMode;
    }

    /**
     *  取回已初始化之device;
     */
    protected SFR300 $() {
        ensureInitialize();
        return device;
    }

    /**
     * 程式結束前一定要卸載 dll
     */
    public static void unload() {
        if (initialized.getAndSet(false)) {
            if (inCapturing.getAndSet(false)) {
                device.SF_AbortCapturing();
            }
            if (inMatching.getAndSet(false)) {
                device.SF_AbortMatching();
            }
            try {
                device.SF_Uninitialize();
            } catch (Exception e) {
                e.printStackTrace();
            }
        }
    }

    /**
     * 是否已初始化
     */
    public static boolean isInitialized() {
        return initialized.get();
    }

    /**
     * 選定掃描器型號
     * @param devType 描器型號
     * @return 是曾切換成功
     */
    public boolean setSelectedDevice(DeviceType devType) {
        reset();
        return $() == null ? false : $().SF_SelectReader(devType).equals(devType);
    }

    /**
     * 取得設備型態
     */
    public DeviceType getDeviceType() {
        ensureInitialize();
        return deviceType;
    }

    /**
     * 回傳設備數量
     */
    public int getDeviceQuantity() {
        return $() == null ? 0 : $().SF_GetDeviceNumber();
    }

    /**
     * 取回目前選定的設備序列號
     * @return 
     */
    public String getSerialNo() {
        StringBuilder sb = new StringBuilder(33);
        byte[] serno = new byte[33];
        Arrays.fill(serno, (byte) 0);
        if ($() != null && $().SF_GetSerial(serno)) {
            for (byte b : serno) {
                if (b > 0) {
                    char c = (char) b;
                    sb.append(String.format("%s", c));
                }
            }
            return sb.toString();
        } else {
            return null;
        }
    }

    /**
     * 取回指紋機等待手指回應時間, SFR_300_V2預設為5.000微秒.
     * @return 等待手指最大微秒(ms)
     */
    public int getTimeout() {
        return $() == null ? Integer.MAX_VALUE : $().SF_GetTimeout();
    }

    /**
     * 設定指紋機等待手指回應時間, SFR_300_V2預設為5.000微秒.
     * @return 等待手指最大微秒(ms)
     */
    public boolean setTimeout() {
        return setTimeout(5000);
    }

    /**
     * 設定手指回應時間
     * @param timeout 等待最大微秒(ms)
     * @return 是否設定成功
     */
    public boolean setTimeout(int timeout) {
        assert timeout > 0 : "手指回應時間必須大於0";
        return $() == null ? false : $().SF_SetTimeout(timeout);
    }

    /**
     * 取回設備之明度度設定，數值愈大，則掃瞄器生成的圖像愈黑.
     * SFR200預設為50,SFR_300預設為100,
     * @return 回傳 0 ~ 255
     */
    public int getBrightness() {
        return $() == null ? 0 : $().SF_GetBrightness();
    }

    /**
     * 設定設備之明度度
     * @return 明度度(0 ~ 255)
     */
    public boolean setBrightness() {
        return $() == null ? false : setBrightness(100);
    }

    /**
     * 設定設備之明度度.
     * @param brightness 必須介於 0~255
     * @return 是否設定成功
     */
    public boolean setBrightness(int brightness) {
        assert brightness > -1 && brightness < 256 : "明亮度之值必須介於 0 ~ 255";
        return $() == null ? false : $().SF_SetBrightness(brightness);
    }

    /**
     * 取得設備之感光度，感光度之值愈高則愈敏感
     * 預設值為 4
     * @return 回傳值 0 ~ 7
     */
    public int getSensitivity() {
        return $() == null ? 0 : $().SF_GetSensitivity();
    }

    /**
     * 設定設備之敏感度
     * @param sensitivity 敏感度之值必須介於0 ~ 7
     * @return 是否設定成功
     */
    public boolean setSensitivity(int sensitivity) {
        assert sensitivity > -1 && sensitivity < 8 : "敏感度之值必須介於0 ~ 7";
        return $() == null ? false : $().SF_SetSensitivity(sensitivity);
    }

    /**
     * 取回選定型號設備之索引值.
     * 該型號之第一台設備索引值為0(從0開始)
     * @return 是否設定成功
     */
    public int getDeviceIndex() {
        return $() == null ? -1 : $().SF_GetDevice();
    }

    /**
     * 取回選定型號設備之索引值
     * @param index 索引值(從0開始)
     * @return 是否設定成功
     */
    public boolean setDeviceIndex(int index) {
        assert index > -1 && index < getDeviceQuantity() : "索引值不可能小於零，也不可能大於所有設備總量";
        return $() == null ? false : $().SF_SetDevice(index);
    }

    /**
     * 檢查裝置是否連接
     * @return  裝置是否連接
     */
    public boolean isSensorOn() {
        return $() == null ? false : $().SF_IsSensorOn();
    }

    /**
     * 檢查手指是否置於裝置上
     * @return 手指是否置於裝置上
     */
    public boolean isFingerOn() {
        return $() == null ? false : $().SF_IsFingerOn();
    }

    /**
     * 清除指紋機內暫存圖像
     */
    public void clear() {
        if ($() != null && getImageWidth() > 0) {
            logger.debug("清除圖像資料{}x{}", getImageWidth(), getImageHeight());
            captured.set(false);
            $().SF_Clear();
        }
    }

    /**
     * 補捉影像
     * @return 是否成功 
     */
    public boolean capture() {
        if ($() == null) {
            return false;
        } else {
            if (!inCapturing.getAndSet(true)) {
                fireCaptureEvent(new CaptureEvent(this));
                captured.set($().SF_Capture());
                inCapturing.set(false);
                fireCaptureEvent(new CaptureEvent(this, false));
                logger.debug("掃瞄{}", captured.get() ? "完成" : "失敗");
            }
            return captured.get();
        }
    }

    /**
     * 停止擷取影像
     */
    public void abortCapturing() {
        if ($() != null && inCapturing.getAndSet(false)) {
            logger.debug("中斷擷取影像");
            $().SF_AbortCapturing();
            logger.debug("影像擷取取消");
            fireCaptureEvent(new CaptureEvent(this, false));
//            inCapturing.set(false);
        }
    }

    /**
     * 繪製指紋, 
     * * 此funcion應置於 {@link Component}.{@link Component#paint(java.awt.Graphics) }({@link Graphics})內叫用
     * @param c  繪製標的，必須為HeavyWeight Component
     * @param l  左上角的x座標
     * @param t  左上角的y座標
     * @param r  右下角的x座標
     * @param b 右下角的y座標
     * @param core Specifies whether the core of fingerprint is drawn of not.
     * 指定是否繪製指紋核心，範例上的標籤用字為&quot;偵測核心(detect core)&quot;
     */
    public void draw(Component c, int l, int t, int r, int b, boolean core) {
        if ($() != null && c.isDisplayable()) {
            HWND hwnd = new HWND();
            hwnd.setPointer(Native.getComponentPointer(c));
            $().SF_Draw(hwnd, l, t, r, b, core);
        }
    }

    /**
     * 繪製指紋, 
     * 此funcion應置於 {@link Component}.{@link Component#paint(java.awt.Graphics) }({@link Graphics})內叫用
     * @param c 繪製標的，必須為HeavyWeight Component
     * @param g compent c 的繪製區域
     * @param core Specifies whether the core of fingerprint is drawn of not.  
     * 指定是否繪製指紋核心，範例上的標籤用字為&quot;偵測核心(detect core)&quot;
     */
    public void draw(Component c, Graphics g, boolean core) {
        if ($() != null) {
            Dimension d = c.getSize();
            if (isInitialized() && d.width > 0 && d.height > 0) {
                g.clearRect(0, 0, d.width, d.height);
                draw(c, 0, 0, d.width, d.height, core);
            }
        }
    }

    /**
     * 取得掃瞄影像內容
     * @return 掃瞄影像
     */
    public RenderedImage getImageBuffer() {
        int size = isInitialized() ? getImageWidth() * getImageHeight() : 0;
        assert size > 0 : "取圖檔前必須先進行指紋掃瞄";
        //從範例程式可看出，調色盤盤為 0~255之灰階
        byte[] colors = device.SF_GetImageBuffer().getByteArray(0, size);
        int height = getImageHeight();
        int width = getImageWidth();
        //建立調色盤
        byte[] palette = new byte[256];
        for (int i = 0; i < palette.length; i++) {
            palette[i] = (byte) i;
        }
        IndexColorModel model = new IndexColorModel(8, palette.length, palette, palette, palette);
        BufferedImage image = new BufferedImage(width, height, BufferedImage.TYPE_BYTE_INDEXED, model);
        image.getRaster().setDataElements(0, 0, width, height, colors);
        return image;
    }

    /**
     * 將指紋機之緩之圖存檔，因為java image 處理 GIF限定為8-bit圖檔，格式最小，所以最好存為gif
     * @param file 輸出之檔案路徑名稱
     * @return 輸出檔案
     * @throws IOException 
     */
    public File saveGif(String file) throws IOException {
        File f = new File(file);
        saveGif(f);
        return f;
    }

    /**
     * 將指紋機之緩之圖存檔，因為java image 處理 GIF限定為8-bit圖檔，格式最小，所以最好為gif
     * @param file 輸出檔案
     * @return 輸出檔案
     */
    public File saveGif(File file) throws IOException {
        return saveImage("gif", file);
    }

    /**
     * 將指紋機之緩之圖存檔，因為java image 處理 GIF限定為8-bit圖檔，格式最小，所以最好為gif
     * @param output 輸出串流
     */
    public void safeGif(OutputStream output) throws IOException {
        ImageIO.write(getImageBuffer(), "gif", output);
    }

    /**
     * 輸出圖檔(只支援JPEG, PNG, GIF, BMP 與 WBMP)
     * @param formatName 輸出格式
     * @param file 輸出檔案
     * @return 輸出檔案
     * @throws IOException 
     */
    public File saveImage(String formatName, File file) throws IOException {
        ImageIO.write(getImageBuffer(), formatName, file);
        return file;
    }

    /**                          
     * 輸出圖檔(只支援JPEG, PNG, GIF, BMP 與 WBMP)
     * @see ImageIO#write(java.awt.image.RenderedImage, java.lang.String, javax.imageio.stream.ImageOutputStream) 
     * @param formatName 輸出格式
     * @param output 輸出串流
     */
    public void saveImage(String formatName, ImageOutputStream output) throws IOException {
        ImageIO.write(getImageBuffer(), formatName, output);
    }

    /**
     * 掃瞄影像高度
     * @return 掃瞄影像高度
     */
    public int getImageHeight() {
        return device == null ? 0 : device.SF_GetImageHeight();
    }

    /**
     * 掃瞄影像寬度
     * @return 掃瞄影像寬度
     */
    public int getImageWidth() {
        return device == null ? 0 : device.SF_GetImageWidth();
    }

    /**
     * 從指紋機內存取出已掃瞄之取樣資料.<br/>
     * 範例:<br/>
     * device.{@link #clear()};<br/>
     * logger.debug("******請放入要登記的手指)******");<br/>
     * if (device.{@link #capture() }) {<br/>
     * &nbsp;&nbsp;&nbsp;&nbsp;{@link Template} temp = device.{@link #enroll(boolean) enroll}(false);<br/>
     * &nbsp;&nbsp;&nbsp;&nbsp;if (device.{@link #isFingerOn() }) {<br/>
     * &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;logger.debug("請挪開手指");<br/>
     * &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;while (device.{@link #isFingerOn() }) {<br/>
     * &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;}<br/>
     * &nbsp;&nbsp;&nbsp;&nbsp;}<br/>
     * &nbsp;&nbsp;&nbsp;&nbsp;logger.debug("請將要登記的手指再次放入,進行再次確認");<br/>
     * &nbsp;&nbsp;&nbsp;&nbsp;while (!device.{@link #isFingerOn()}) {<br/>
     * &nbsp;&nbsp;&nbsp;&nbsp;}<br/>
     * &nbsp;&nbsp;&nbsp;&nbsp;if (device.{@link #capture()}) {<br/>
     * &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;try {<br/>
     * &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;temp = device.{@link #enrollWithVerify(com.suprema.biomini.Template, boolean) enrollWithVerify}(temp, false);<br/>
     * &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;logger.debug("登記結果{}", temp.showBytes());<br/>
     * &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;} catch ({@link EnrollType} e) {<br/>
     * &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;logger.error("比對失敗", e);<br/>
     * &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;}<br/>
     * &nbsp;&nbsp;&nbsp;&nbsp;}<br/>
     * }
     * @param coreDetect 是否進行指紋定位偵測(檢查中心，四邊是否缺損)
     * @return 指紋樣本資料
     * @throws EnrollException 取樣品質不足錯誤時是進行定位偵測發現指紋缺損
     */
    public Template enroll(boolean coreDetect) throws EnrollException {
        return enroll(coreDetect, 50);
    }

    /**
     * 從指紋機內存取出已掃瞄之取樣資料
     * @param coreDetect  是否進行指紋定位偵測(檢查中心，四邊是否缺損)
     * @param quality 取樣品質，須介於30~100(30誤判率高，至少應設為50)
     * @return 指紋樣本資料
     * @throws EnrollException 取樣品質不足錯誤時是進行定位偵測發現指紋缺損
     */
    public Template enroll(boolean coreDetect, int quality) throws EnrollException {
        assert $() != null : "找不到指紋掃瞄器";
        assert getImageHeight() * getImageWidth() > 0 : "不存在掃瞄取樣資料";
        assert quality > 29 && quality < 101 : "取樣品質必須介於30~100";
        IntByReference size = new IntByReference();
        Template template = new Template();
        EnrollType result = $().SF_Enroll(template.getData(), size, coreDetect);
        template.setSize(size.getValue());
        if (EnrollType.SF_SUCCESS.equals(result)) {
            int q = getEnrollQuality();
            if (q < quality) {
                abortCapturing();
                throw new BadEnrollQualityException(String.format("取樣品質(%d)低於 %d", q, quality));
            } else {
                logger.debug("取樣品質:{},取樣大小:{}", q, template.getSize());
                return template;
            }
        } else if (EnrollType.SF_NOTGOODIMAGE.equals(result)) {
            abortCapturing();
            throw new BadEnrollQualityException("取樣品質不良", result);
        } else {
            abortCapturing();
            throw new EnrollException(result);
        }
    }

    /**
     * 啟動掃瞄待取得指紋後與樣本參數進行比對，比對符合則傳回取樣資料，否則回傳null,或引發錯誤,見
     * {@link #enroll(boolean) }
     * @param template 原始指紋樣本
     * @param coreDetect 是否進行指紋定位偵測(檢查中心，四邊是否缺損)
     * @return 新掃瞄的指紋樣本資料
     * @throws EnrollException {@link EnrollException}
     */
    public Template enrollWithVerify(Template template, boolean coreDetect) throws EnrollException {
        return enrollWithVerify(template, coreDetect, 50);
    }

    /**
     * 
     * 啟動掃瞄待取得指紋後與樣本參數進行比對，比對符合則傳回取樣資料，否則回傳null,或引發錯誤
     * @param template 原始指紋樣本
     * @param coreDetect 是否進行指紋定位偵測(檢查中心，四邊是否缺損)
     * @param quality 要求掃瞄之品質至少達到的標準(30~100,但30誤判率高，至少應設為50)
     * @return 新掃瞄的指紋樣本資料
     * @throws EnrollException {@link EnrollException}
     */
    public Template enrollWithVerify(Template template, boolean coreDetect, int quality) throws EnrollException {
        assert $() != null : "找不到指紋掃瞄器";
        assert template != null && template.getSize() > 0 : "比對取樣資料不可為空";
        assert quality > 29 && quality < 101 : "取樣品質必須介於30~100";
        logger.debug("原取樣大小:{}", template.getSize());
        IntByReference size = new IntByReference(template.getSize());
        Template temp = template.clone();
        //第二次取樣較好時，temp內容會被取代
        EnrollType result = device.SF_EnrollWithVerify(temp.data, size, coreDetect);
        logger.debug("比對後取樣結果:{}", result);
        if (EnrollType.SF_SUCCESS.equals(result)) {
            abortCapturing();
            int q = getEnrollQuality();
            if (q < quality) {
                throw new BadEnrollQualityException(String.format("取樣品質(%d)低於 %d", q, quality));
            } else {
                logger.debug("取樣品質:{}", q);
                return temp;
            }
        } else if (EnrollType.SF_VERIFYFAIL.equals(result)) {
            abortCapturing();
            return null;
        } else if (EnrollType.SF_NOTGOODIMAGE.equals(result)) {
            abortCapturing();
            throw new BadEnrollQualityException("取樣品質不良", result);
        } else {
            abortCapturing();
            throw new EnrollException(result);
        }
    }

    /**
     * 指紋掃瞄後以錯誤接受率0.001%進行 1:1 比對.<br/>
     * 範例:<br/>
     * device.{@link #clear() };<br/>
     * //從 某處 載入以前建立的樣本資料<br/>
     * Template sample = ...;<br/>
     * if (!device.{@link #isFingerOn() }) {<br/>
     * &nbsp;&nbsp;&nbsp;&nbsp;logger.debug("請放入要比對的手指");<br/>
     * }<br/>
     * if (device.{@link #isCaptured() }) {<br/>
     * &nbsp;&nbsp;&nbsp;&nbsp;if(device.{@link #verify(com.suprema.biomini.Template, boolean) verify}(sample, false)){<br/>
     * &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;logger.debug("比對符合");<br/>
     * &nbsp;&nbsp;&nbsp;&nbsp;} else {<br/>
     * &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;logger.error("比對不符");<br/>
     * &nbsp;&nbsp;&nbsp;&nbsp;}<br/>
     * }
     * @param template 比對的母樣本
     * @param coreDetect  是否進行指紋定位偵測(檢查中心，四邊是否缺損)
     * @return 是否比對成功
     */
    public boolean verify(Template template, boolean coreDetect) {
        return verify(template, SecurityLevel.FAR4, coreDetect);
    }

    /**
     * 指紋掃瞄後進行比對(1:1)
     * @param template 比對的母樣本
     * @param far 指示可接受之錯誤接受率(False Accept Rate)等級
     * @param coreDetect  是否進行指紋定位偵測(檢查中心，四邊是否缺損)
     * @return 是否比對成功
     */
    public boolean verify(Template template, SecurityLevel far, boolean coreDetect) {
        assert isCaptured() : "比對前請先進行指紋掃瞄";
        return EnrollType.SF_SUCCESS.equals($().SF_Verify(template.getData(), far, coreDetect));
    }

    /**
     * 指紋掃瞄後進行以錯誤接受率0.001%比對(1:1)，若比對失敗則發出失敗異常
     * @param template 比對的母樣本
     * @param coreDetect  是否進行指紋定位偵測(檢查中心，四邊是否缺損)
     * @throws EnrollException 失敗時發出錯誤
     */
    public void verifyWithError(Template template, boolean coreDetect) throws EnrollException {
        verifyWithError(template, SecurityLevel.FAR4, coreDetect);
    }

    /**
     * 指紋掃瞄後進行比對(1:1)，若比對失敗則發出失敗異常
     * @param template 比對的母樣本
     * @param far 指示可接受之錯誤接受率(False Accept Rate)等級，參考{@link SecurityLevel}
     * @param coreDetect  是否進行指紋定位偵測(檢查中心，四邊是否缺損)
     * @throws EnrollException 失敗時發出錯誤
     */
    public void verifyWithError(Template template, SecurityLevel far, boolean coreDetect) throws EnrollException {
        assert isCaptured() : "比對前請先進行指紋掃瞄";
        EnrollType res = $().SF_Verify(template.getData(), far, coreDetect);
        if (EnrollType.SF_NOTGOODIMAGE.equals(res)) {
            throw new BadEnrollQualityException(res);
        } else if (!EnrollType.SF_SUCCESS.equals(res)) {
            throw new EnrollException(res);
        }
    }

    /**
     * 以 1:N 方式進行比對
     * @param templates 要比對的樣本資料，會載入記憶體中，當樣本母體太大時，請切割樣本母體分次比對，以降低記憶體需求
     * @param far far 指示可接受之錯誤接受率(False Accept Rate)等級，參考{@link SecurityLevel}
     * @param coreDetect 是否進行指紋定位偵測(檢查中心，四邊是否缺損)
     * @param timeout 最大比對時間(ms:毫秒)，超過時間未完成比對則判定比對失敗，設為0時，則無逾時之虞
     * @param multiThred 是否以多執行緒進行比對
     * @return 比對成功時回傳符合之指紋資料，比對不成時回傳null
     * @throws EnrollException 比對時發生異常，比如影像不清或是資料缺損
     */
    public Template identify(List<Template> templates, SecurityLevel far, boolean coreDetect, int timeout, boolean multiThred) throws EnrollException {
        assert isCaptured() : "比對前請先進行指紋掃瞄";
        timeout = timeout < 0 ? 0 : timeout;
        Memory chunk = new Memory(templates.size() * Pointer.SIZE);
        for (int i = 0; i < templates.size(); i++) {
            Memory piece = new Memory(SFR300.SF_TEMPLATESIZE);
            piece.write(0, templates.get(i).getData(), 0, SFR300.SF_TEMPLATESIZE);
            chunk.setPointer(i * Pointer.SIZE, piece);
        }
        logger.debug("寫入記憶體完成");
        IntByReference match = new IntByReference(-1);
        if (!inMatching.getAndSet(true)) {
            EnrollType res = multiThred
                    ? device.SF_IdentifyMT(chunk, templates.size(), match, SecurityLevel.FAR4, coreDetect, timeout)
                    : device.SF_Identify(chunk, templates.size(), match, SecurityLevel.FAR4, coreDetect, timeout);
            if (EnrollType.SF_SUCCESS.equals(res)) {
                return templates.get(match.getValue());
            } else if (EnrollType.SF_VERIFYFAIL.equals(res)) {
                return null;
            } else if (EnrollType.SF_NOTGOODIMAGE.equals(res)) {
                throw new BadEnrollQualityException(res);
            } else {
                throw new EnrollException(res);
            }
        } else {
            throw new EnrollException("上一次比對仍在進行中，請稍後再比對", EnrollType.SF_VERIFYFAIL);
        }
    }

    /**
     * 以 1:N 方式進行比對
     * @param sources 指紋來源
     * @param far far 指示可接受之錯誤接受率(False Accept Rate)等級，參考{@link SecurityLevel}
     * @param coreDetect 是否進行指紋定位偵測(檢查中心，四邊是否缺損)
     * @param timeout 最大比對時間(ms:毫秒)，超過時間未完成比對則判定比對失敗，設為0時，則無逾時之虞
     * @param multiThred 是否以多執行緒進行比對
     * @return 比對成功時回傳符合之指紋資料，比對不成時回傳null
     * @throws EnrollException 比對時發生異常，比如影像不清或是資料缺損
     */
    public Template identify(Supplier<Template> sources, SecurityLevel far, boolean coreDetect, int timeout, boolean multiThred) throws EnrollException {
        assert isCaptured() : "比對前請先進行指紋掃瞄";
        Template result = null;
        while (sources.hasNext() & result == null) {
            List<Template> temps = sources.request();
            result = identify(temps, far, coreDetect, timeout, multiThred);
        }
        return result;
    }

    /**
     * 若在進行 1:N比對時，強制取消比對
     */
    public void abortMatching() {
        if (inMatching.getAndSet(false)) {
            device.SF_AbortMatching();
        }
    }

    /**
     * 取樣品質，只在叫用{@link #enroll(boolean) enroll}、
     * {@link #enrollWithVerify(com.suprema.biomini.Template, boolean) enrollWithVerify}，
     * {@link #verify(com.suprema.biomini.Template, boolean) verify}、
     * {@link #verifyWithError(com.suprema.biomini.Template, boolean) verifyWithError}
     * 與{@link #identify(java.util.List, com.suprema.biomini.SecurityLevel, boolean, int, boolean) identify}之後有效
     * @return 1~100,低於30為品質不良，高於50則品質良好
     */
    public int getEnrollQuality() {
        return $() == null ? 1 : $().SF_GetEnrollQuality();
    }

    /**
     * 掃瞄取樣旋轉180°
     * @param template 
     */
    public void rotateTemplate(Template template) {
        if ($() != null) {
            $().SF_RotateTemplate(template.getData());
        }
    }

    /**
     * 儲存取樣資料到檔案
     * @param template 取樣資料
     * @param path 檔案路徑
     * @throws FileNotFoundException
     * @throws IOException 
     */
    public void saveTemplate(Template template, String path) throws FileNotFoundException, IOException {
        saveTemplate(template, path, true);
    }

    /**
     * 儲存取樣資料到檔案
     * @param template 取樣資料
     * @param path 檔案路徑
     * @param overwriteExists 檔案存在時是否覆寫
     * @throws FileNotFoundException
     * @throws IOException 
     */
    public void saveTemplate(Template template, String path, boolean overwriteExists) throws FileNotFoundException, IOException {
        assert template != null && template.getSize() > 0 : "取樣資料為空";
        File f = new File(path);
        saveTemplate(template, f, overwriteExists);
    }

    /**
     * 儲存取樣資料到檔案
     * @param template 取樣資料
     * @param file 檔案
     * @throws FileNotFoundException
     * @throws IOException 
     */
    public void saveTemplate(Template template, File file) throws FileNotFoundException, IOException {
        saveTemplate(template, file, true);
    }

    /**
     * 儲存取樣資料到檔案
     * @param template 取樣資料
     * @param file 檔案
     * @param overwriteExists 檔案存在時是否覆寫
     * @throws FileNotFoundException
     * @throws IOException 
     */
    public void saveTemplate(Template template, File file, boolean overwriteExists) throws FileNotFoundException, IOException {
        if (!file.exists() || overwriteExists) {
            ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream(file));
            try {
                oos.writeObject(template);
            } finally {
                oos.close();
            }
        }
    }

    /**
     * 從檔案載回取樣資料
     * @param path 檔案路徑名稱
     * @return
     * @throws IOException
     * @throws ClassNotFoundException 
     */
    public Template loadTemplate(String path) throws IOException, ClassNotFoundException {
        ObjectInputStream ois = new ObjectInputStream(new FileInputStream(path));
        try {
            return (Template) ois.readObject();
        } finally {
            ois.close();
        }
    }

    /**
     * 從檔案載回取樣資料
     * @param file
     * @return
     * @throws IOException
     * @throws ClassNotFoundException 
     */
    public Template loadTemplate(File file) throws IOException, ClassNotFoundException {
        ObjectInputStream ois = new ObjectInputStream(new FileInputStream(file));
        try {
            return (Template) ois.readObject();
        } finally {
            ois.close();
        }
    }

    /**
     * 加入監聽掃瞄開始或結束(包含掃瞄中斷);
     * @param listener 監聽器
     */
    public void addCaptureListener(CaptureListener listener) {
        listenerList.add(CaptureListener.class, listener);
    }

    /**
     * 移除監聽掃瞄開始或結束(包含掃瞄中斷);
     * @param listener 監聽器
     */
    public void removeMyEventListener(CaptureListener listener) {
        listenerList.remove(CaptureListener.class, listener);
    }

    /**
     * 觸發掃瞄事件
     * @param event 掃瞄事件
     */
    protected void fireCaptureEvent(CaptureEvent event) {
        Object[] listeners = listenerList.getListenerList();
        for (int i = 0; i < listeners.length; i += 2) {
            if (listeners[i] == CaptureListener.class) {
                ((CaptureListener) listeners[i + 1]).onCapture(event);
            }
        }
    }
}
